//
// Created by Göksu Güvendiren on 2019-05-14.
//

#include "Scene.hpp"
#include <cassert>
extern const float EPSILON;
std::random_device dev;

void Scene::buildBVH() {
    printf(" - Generating BVH...\n\n");
    this->bvh = new BVHAccel(objects, 1, BVHAccel::SplitMethod::NAIVE);
}

Intersection Scene::intersect(const Ray &ray) const
{
    return this->bvh->Intersect(ray);
}

void Scene::sampleLight(Intersection &pos, float &pdf) const
{
    float emit_area_sum = 0;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        if (objects[k]->hasEmit()){
            emit_area_sum += objects[k]->getArea();
        }
    }
    float p = get_random_float() * emit_area_sum;
    emit_area_sum = 0;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        if (objects[k]->hasEmit()){
            emit_area_sum += objects[k]->getArea();
            if (p <= emit_area_sum){
                objects[k]->Sample(pos, pdf);
                break;
            }
        }
    }
}

bool Scene::trace(
        const Ray &ray,
        const std::vector<Object*> &objects,
        float &tNear, uint32_t &index, Object **hitObject)
{
    *hitObject = nullptr;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        float tNearK = kInfinity;
        uint32_t indexK;
        Vector2f uvK;
        if (objects[k]->intersect(ray, tNearK, indexK) && tNearK < tNear) {
            *hitObject = objects[k];
            tNear = tNearK;
            index = indexK;
        }
    }


    return (*hitObject != nullptr);
}

// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Intersection p = intersect(ray);
    if(!p.happened){
        return backgroundColor;
    }
    else if(p.m->hasEmission()){
        return p.m->getEmission();
    }
    return shade(p.coords + (dotProduct(p.normal, ray.direction) < 0 ? EPSILON * p.normal: -EPSILON * p.normal), -ray.direction, p.m, p.normal);
}

Vector3f Scene::shade(const Vector3f& pos, const Vector3f& dir, Material* mptr, const Vector3f& N) const{ // the dir is outwards
    Vector3f light_dir, light_indir;
    Intersection sample_light;
    float pdf_value;
    sampleLight(sample_light, pdf_value);
    Vector3f light_to_pos = pos - sample_light.coords;
    float distance_2 = light_to_pos.norm();

    Intersection block = intersect({pos, -normalize(light_to_pos)});
    if((block.distance + 0.00016f) >= distance_2){
        light_dir = sample_light.emit * mptr->eval(light_to_pos, dir, N) * dotProduct(normalize(-light_to_pos), N) * \
        dotProduct(normalize(light_to_pos), sample_light.normal) / std::pow(distance_2, 2) / pdf_value;
        light_dir.x = std::max(light_dir.x, 0.f);
        light_dir.y = std::max(light_dir.y, 0.f);
        light_dir.z = std::max(light_dir.z, 0.f);
        assert(light_dir.x >= 0 && light_dir.y >= 0 && light_dir.z >=0);   
    }
    float tmp = get_random_float();
    if(tmp <= RussianRoulette){
        Vector3f sample_wo = mptr->sample(-dir, N);
        Intersection new_pos = intersect({pos, sample_wo});
        if(new_pos.happened && !new_pos.m->hasEmission()){
            light_indir = shade(new_pos.coords + \
            (dotProduct(new_pos.normal, sample_wo) < 0 ? EPSILON * new_pos.normal : -EPSILON * new_pos.normal), -sample_wo, new_pos.m, new_pos.normal);
            light_indir = light_indir * mptr->eval(-sample_wo, dir, N) * dotProduct(normalize(sample_wo), N) / mptr->pdf(-sample_wo, dir, N) / RussianRoulette;     
            assert(light_indir.x >= 0 && light_indir.y >= 0 && light_indir.z >= 0);
        }
    }
    return light_dir + light_indir;
}